---
title: How (not) to use translations outside of React components
---

# How (not) to use translations outside of React components

<small>Apr 21, 2023 · by Jan Amann</small>

Have you ever wondered why `next-intl` doesn’t provide an API to consume translations outside of React components?

The traditional way to internationalize your app with `next-intl` is to use the `useTranslations` hook:

```tsx
import {useTranslations} from 'next-intl';

function About() {
  const t = useTranslations('About');
  return <h1>{t('title')}</h1>;
}
```

Why is it not possible to format messages e.g. in utility functions? Is there something missing here?

This may seem like an unnecessary limitation, but the absence of this feature is intentional and aims to encourage the use of proven patterns that avoid potential issues—especially if they are easy to overlook.

## Example: Formatting error messages

Let's assume you have a `FeedbackForm` component that posts user feedback to a backend endpoint. Unfortunately, the server occasionally returns a 504 status code due to the high volume of feedback. To improve the user experience, you would like to implement automatic retries and provide appropriate feedback to the user.

Here’s a naive approach. Can you spot an issue with this implementation?

```tsx
import {useTranslations, useNow} from 'next-intl';
import {addMinutes} from 'date-fns';

function sendFeedback() {
  // ❌ Bad implementation: Returns formatted messages
  API.sendFeedback().catch((error) => {
    // In case of a gateway timeout, notify the
    // user that we'll try again in 5 minutes
    if (error.status === 504) {
      // (let's assume `t` is defined here for the sake of the example)
      return t('timeout', {nextAttempt: addMinutes(new Date(), 5)});
    }
  });
}

function FeedbackForm({user}) {
  const t = useTranslations('Form');
  const [errorMessage, setErrorMessage] = useState();

  function onSubmit() {
    sendFeedback().catch((errorMessage) => {
      setErrorMessage(errorMessage);
    });
  }

  return (
    <form onSubmit={onSubmit}>
      {errorMessage != null && <p>{errorMessage}</p>}
      ...
    </form>
  );
}
```

Have you found an issue?

Let's have a look together:

1. The `nextAttempt` value is interpolated into the message in a utility function that is called by an event handler. There's no way how we can keep the remaining time updated as we're nearing the retry timeout.
2. If the user changes the language, the error message will remain in the previously selected language, leading to a jarring user experience.

## A better way: Formatting during render

To avoid these issues, we can format messages during the rendering phase of React, turning data structures into human readable strings.

```tsx
import {useTranslations, useNow} from 'next-intl';
import {addMinutes} from 'date-fns';

function FeedbackForm({user}) {
  const t = useTranslations('Form');
  const [retry, setRetry] = useState();
  const now = useNow({
    // Update every minute
    updateInterval: 1000 * 60
  });

  function onSubmit() {
    // ✅ Good implementation: Store data structures in state
    API.sendFeedback().catch((error) => {
      if (error.status === 504) {
        setRetry(addMinutes(now, 5));
      }
    });
  }

  return (
    <form onSubmit={onSubmit}>
      {retry != null && <p>{t('timeout', {nextAttempt: nextAttempt - now})}</p>}
      ...
    </form>
  );
}
```

Now, we can offer a better user experience by interactively counting down the time to the next attempt.

Additionally, this approach is more robust to possibly unexpected states, like the user changing the language while the timeout message is being displayed.

## The exception that proves the rule

If you’re working with Next.js, you might want to translate i18n messages in [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes), [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) or the [Metadata API](https://nextjs.org/docs/app/api-reference/file-conventions/metadata).

`next-intl/server` provides a set of awaitable versions of the functions that you usually call as hooks from within components. These are agnostic from React and can be used for these cases.

```tsx
import {getTranslations} from 'next-intl/server';

// The `locale` is received from Next.js via `params`
const locale = params.locale;

// This creates the same function that is returned by `useTranslations`.
const t = await getTranslations({locale});

// Result: "Hello world!"
t('hello', {name: 'world'});
```

## This seems familiar

If you’ve been working with React for a longer time, you might have experienced the change [from `component{DidMount,DidUpdate,WillUnmount}` to `useEffect`](https://legacy.reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update). The reason why `useEffect` is superior is because it nudges the developer into a direction where the app is always in sync and by doing this, a whole array of potential issues just magically disappear.

By limiting ourselves to only format messages during render, we're in a similar situation: The rendered output of translated messages is always in sync with app state and we can rely on the app being consistent.

Related: ["How can I reuse messages?" in the structuring messages docs](/docs/usage/messages#structuring-messages)
